// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).

using System;
using System.Linq;

using Tierless.Framework.Entities;
using Tierless.Framework.EntityTypes;
using Tierless.Framework.Constraints;
using Tierless.Framework.Utilities;

namespace Tierless.Framework.Server.Constraints
{
	/// <summary>
	/// Provides the enforcement of the source side of a reference constraint.
	/// </summary>
	/// <remarks>
	/// The source side of a reference constraint is enforced on insert or update of an entity
	/// of the source entity type. The source columns are used to construct an entity ID based
	/// on the target entity name and target entity columns. The target entity type is searched
	/// for an entity matching the constructed ID. If one is found, the constraint is satisfied.
	/// Otherwise, a constraint violation is returned.
	/// </remarks>
    public class SourceReference : EntityConstraint
    {
		/// <summary>
		/// Initializes a new instance of the SourceReference class.
		/// </summary>
		/// <param name="entityTypeName">The name of the entity type from which the reference originates.</param>
		/// <param name="columnNames">The name of the referencing columns in the source entity type.</param>
		/// <param name="targetEntityTypeName">The name of the entity type being referenced.</param>
		/// <param name="targetColumnNames">The name of the referenced columns in the target entity type.</param>
		/// <param name="severity">The severity of a violation.</param>
		/// <param name="message">The message for a violation.</param>
        public SourceReference(string entityTypeName, string[] columnNames, string targetEntityTypeName, string[] targetColumnNames, ViolationSeverity severity, string message)
            : base
            (
                String.Format("{0}{1}_{2}{3}SourceReference", entityTypeName, columnNames.Combine(), targetEntityTypeName, targetColumnNames.Combine()),
                entityTypeName,
                columnNames,
                severity,
                message,
                ConstraintLocation.Server,
                ConstraintTransition.InsertUpdate
            )
        {
            TargetEntityTypeName = targetEntityTypeName;
            TargetColumnNames = targetColumnNames;
        }

		/// <summary>
		/// Initializes a new instance of the SourceReference class. This constructor provides a default constraint violation message.
		/// </summary>
		/// <param name="entityTypeName">The name of the entity type from which the reference originates.</param>
		/// <param name="columnNames">The name of the referencing columns in the source entity type.</param>
		/// <param name="targetEntityTypeName">The name of the entity type being referenced.</param>
		/// <param name="targetColumnNames">The name of the referenced columns in the target entity type.</param>
		/// <param name="severity">The severity of a violation.</param>
        public SourceReference(string entityTypeName, string[] columnNames, string targetEntityTypeName, string[] targetColumnNames, ViolationSeverity severity)
            : this
            (
                entityTypeName,
                columnNames,
                targetEntityTypeName,
                targetColumnNames,
                severity,
                String.Format("The referenced {0} does not exist.", targetEntityTypeName)
            )
        { }

		/// <summary>
		/// Initializes a new instance of the SourceReference class. This constructor provides a default constraint violation message as well as a default error severity.
		/// </summary>
		/// <param name="entityTypeName">The name of the entity type from which the reference originates.</param>
		/// <param name="columnNames">The name of the referencing columns in the source entity type.</param>
		/// <param name="targetEntityTypeName">The name of the entity type being referenced.</param>
		/// <param name="targetColumnNames">The name of the referenced columns in the target entity type.</param>
        public SourceReference(string entityTypeName, string[] columnNames, string targetEntityTypeName, string[] targetColumnNames)
            : this
            (
                entityTypeName,
                columnNames,
                targetEntityTypeName,
                targetColumnNames,
                ViolationSeverity.Error
            )
        { }

		/// <summary>
		/// Gets the name of the entity type being referenced.
		/// </summary>
    	public string TargetEntityTypeName { get; private set; }

		/// <summary>
		/// Gets the names of the referenced columns in the target entity type.
		/// </summary>
    	public string[] TargetColumnNames { get; private set; }

    	protected override bool ShouldValidate(ConstraintContext context)
		{
			if (base.ShouldValidate(context))
				return context.Transition != ConstraintTransition.Update || ColumnNames.Any(AColumn => !Equals(context.Entity[AColumn].OriginalValue, context.Entity[AColumn].Value));

    		return false;
		}

        protected override bool IsValid(ConstraintContext context)
        {
			var targetValues = new object[ColumnNames.Length];
			var isNull = false;
			for (var i = 0; i < targetValues.Length; i++)
			{
				targetValues[i] = context.Entity[ColumnNames[i]].Value;
				if (targetValues[i] == null)
					isNull = true;
			}
			
			var targetId = new EntityID(TargetColumnNames, targetValues);
			
            return isNull || (context.FindEntity(TargetEntityTypeName, targetId) != null);
        }

		protected override ConstraintViolation GetViolation(ConstraintContext context)
		{
			return 
				new ConstraintViolation
				(
					Name, 
					EntityTypeName, 
					context.ColumnNames, 
					ColumnNames, 
					context.Severity, 
					String.Format
					(
						"There is no {0} with the selected ID ({1}).", 
						TargetEntityTypeName, 
						new EntityID(context.Entity, ColumnNames)
					)
				);
		}
    }
}
